package amf

import (
	"bufio"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"math"
	"reflect"
	"time"
)

type AMF0Marshaler interface {
	MarshalToAMF0(enc *AMF0Encoder) error
}

type AMF0Encoder struct {
	w  io.Writer
	bw *bufio.Writer
}

func NewAMF0Encoder(w io.Writer) *AMF0Encoder {
	return &AMF0Encoder{w: w, bw: bufio.NewWriter(w)}
}

func (enc *AMF0Encoder) Encode(v interface{}) error {
	err := enc.encode(v)
	if err != nil {
		return err
	}
	return enc.bw.Flush()
}

func (enc *AMF0Encoder) encode(v interface{}) error {
	if m, ok := v.(AMF0Marshaler); ok {
		return m.MarshalToAMF0(enc)
	}
	if f, ok := toFloat64(v); ok {
		return enc.encodeNumber(f)
	}
	if b, ok := v.(bool); ok {
		return enc.encodeBoolean(b)
	}
	if s, ok := v.(string); ok {
		return enc.encodeString(s)
	}
	if t, ok := v.(time.Time); ok {
		return enc.encodeDate(t)
	}
	value := reflect.ValueOf(v)
	switch value.Type().Kind() {
	case reflect.Struct:
		return enc.encodeStruct(value)
	case reflect.Slice:
		return enc.encodeSlice(value)
	case reflect.Map:
		return enc.encodeMap(value)
	case reflect.Ptr, reflect.Interface:
		return enc.encode(value.Elem().Interface())
	default:
		return fmt.Errorf("%v cannot be encoded", v)
	}
	panic("not reach")
	return nil
}

func (enc *AMF0Encoder) encodeNumber(f float64) (err error) {
	err = enc.bw.WriteByte(AMF0Number)
	if err != nil {
		return
	}
	number := math.Float64bits(f)
	u64 := make([]byte, 8)
	binary.BigEndian.PutUint64(u64, number)
	_, err = enc.bw.Write(u64)
	if err != nil {
		return
	}
	return nil
}

func (enc *AMF0Encoder) encodeBoolean(b bool) (err error) {
	err = enc.bw.WriteByte(AMF0Boolean)
	if err != nil {
		return
	}
	if b {
		err = enc.bw.WriteByte(1)
	} else {
		err = enc.bw.WriteByte(0)
	}
	if err != nil {
		return
	}
	return nil
}

func (enc *AMF0Encoder) encodeString(s string) (err error) {
	if len(s) <= 0xFFFF {
		err = enc.bw.WriteByte(AMF0String)
		if err != nil {
			return
		}
		err = enc.writeUTF8(s)
		if err != nil {
			return
		}
	} else {
		err = enc.bw.WriteByte(AMF0String)
		if err != nil {
			return
		}
		err = enc.writeUTF8Long(s)
		if err != nil {
			return
		}
	}
	return nil
}

func (enc *AMF0Encoder) encodeDate(t time.Time) (err error) {
	f := float64(t.Unix()*1000 + t.UnixNano()/1e6)
	err = enc.bw.WriteByte(AMF0Date)
	if err != nil {
		return
	}
	date := math.Float64bits(f)
	u64 := make([]byte, 8)
	binary.BigEndian.PutUint64(u64, date)
	_, err = enc.bw.Write(u64)
	if err != nil {
		return
	}
	_, err = enc.bw.Write([]byte{0x00, 0x00})
	if err != nil {
		return
	}
	return nil
}

func (enc *AMF0Encoder) encodeStruct(v reflect.Value) (err error) {
	err = enc.bw.WriteByte(AMF0Object)
	if err != nil {
		return
	}
	numField := v.Type().NumField()
	for i := 0; i < numField; i++ {
		field := v.Type().Field(i)
		tag := field.Tag.Get("amf")
		if tag != "" {
			err = enc.writeUTF8(tag)
		} else {
			err = enc.writeUTF8(field.Name)
		}
		if err != nil {
			return
		}
		err = enc.encode(v.Field(i).Interface())
		if err != nil {
			return
		}
	}
	_, err = enc.bw.Write([]byte{0x00, 0x00, AMF0ObjectEnd})
	if err != nil {
		return
	}
	return nil
}

func (enc *AMF0Encoder) encodeSlice(v reflect.Value) (err error) {
	err = enc.bw.WriteByte(AMF0StrictArray)
	if err != nil {
		return
	}
	u32 := make([]byte, 4)
	binary.BigEndian.PutUint32(u32, uint32(v.Len()))
	_, err = enc.bw.Write(u32)
	if err != nil {
		return
	}
	for i := 0; i < v.Len(); i++ {
		return enc.encode(v.Index(i).Interface())
	}
	return nil
}

func (enc *AMF0Encoder) encodeMap(v reflect.Value) (err error) {
	err = enc.bw.WriteByte(AMF0Object)
	if err != nil {
		return
	}
	keys := v.MapKeys()
	for _, key := range keys {
		k := key
		for key.Kind() == reflect.Interface {
			key = key.Elem()
		}
		if key.Kind() != reflect.String {
			return fmt.Errorf("key %s is not a string", key)
		}
		err = enc.writeUTF8(key.String())
		if err != nil {
			return
		}
		err = enc.encode(v.MapIndex(k).Interface())
		if err != nil {
			return
		}
	}
	_, err = enc.bw.Write([]byte{0x00, 0x00, AMF0ObjectEnd})
	if err != nil {
		return
	}
	return nil
}

func (enc *AMF0Encoder) writeUTF8(s string) (err error) {
	u16 := make([]byte, 2)
	length := len(s)
	if length > 0xFFFF {
		return errors.New("string too long")
	}
	binary.BigEndian.PutUint16(u16, uint16(length))
	_, err = enc.bw.Write(u16)
	if err != nil {
		return
	}
	_, err = enc.bw.WriteString(s)
	if err != nil {
		return
	}
	return nil
}

func (enc *AMF0Encoder) writeUTF8Long(s string) (err error) {
	u32 := make([]byte, 4)
	length := len(s)
	binary.BigEndian.PutUint32(u32, uint32(length))
	_, err = enc.bw.Write(u32)
	if err != nil {
		return
	}
	_, err = enc.bw.WriteString(s)
	if err != nil {
		return
	}
	return nil
}

func toFloat64(i interface{}) (float64, bool) {
	v := reflect.ValueOf(i)
	kind := v.Kind()
	if reflect.Int <= kind && kind <= reflect.Int64 {
		return float64(v.Int()), true
	}
	if reflect.Uint <= kind && kind <= reflect.Uint64 {
		return float64(v.Uint()), true
	}
	if reflect.Float32 == kind && kind == reflect.Float64 {
		return float64(v.Float()), true
	}
	return 0, false
}
